/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.impl;

import static org.junit.Assert.*;
import edu.uoc.sd.fr14.model.Company;
import edu.uoc.sd.fr14.model.CustomerId;
import edu.uoc.sd.fr14.model.FlightId;
import edu.uoc.sd.fr14.model.Nseq;
import edu.uoc.sd.fr14.model.Op;
import edu.uoc.sd.fr14.model.OpId;
import edu.uoc.sd.fr14.model.ProxyId;
import edu.uoc.sd.fr14.model.Quorum;
import edu.uoc.sd.fr14.model.ServerId;
import edu.uoc.sd.fr14.model.ServerState;
import edu.uoc.sd.fr14.model.SubLog;
import edu.uoc.sd.fr14.test.ClockTest;
import edu.uoc.sd.fr14.test.ClusterTest;
import edu.uoc.sd.fr14.test.ProxyTest;
import edu.uoc.sd.fr14.test.ServerStateCreator;
import edu.uoc.sd.fr14.test.ServerTest;
import edu.uoc.sd.fr14.test.TestCalls;

/**
 *
 * @author drodenas
 */
public class ServerImplCommon {
    
    public ServerImplCommon() {
    }
    
    protected final static ServerId OTHER = new ServerId(7);
    protected final static ServerId GREATER = new ServerId(14);
    protected final static ServerId ME = new ServerId(3);
    protected final static ServerId LOWER = new ServerId(1);
    
    protected ServerState lowerState;
    protected ServerState otherState;
    protected ServerState serverState;
    protected Company company;
    protected ClusterTest cluster;
    protected TestCalls testCalls;
    protected StudentServer server;
        
    public void setUp() {
        otherState = ServerStateCreator.createFor(OTHER);
        lowerState = ServerStateCreator.createFor(LOWER);
        serverState = ServerStateCreator.createFor(ME);
        company = serverState.getCompany();
        cluster = new ClusterTest();
        server = new StudentServer(serverState, cluster);
        testCalls = new TestCalls();
    }
    
    public void tearDown() {
        otherState = null;
        serverState = null;
        company = null;
        cluster = null;
        server = null;
        testCalls = null;
    }

    
    /* *********************************************************************
     * *
     * * Auxiliary set role methods
     * *
     * *********************************************************************/

    protected void makeMeLeader() {
        serverState.getRole().setLeader(ME);
        cluster.me(ME);
    }
    
    protected void makeMeFollower(ServerId leader) {
        serverState.getRole().setLeader(leader);
        cluster.me(ME);
    }
    
    protected void makeMeFollower() {
        makeMeFollower(OTHER);
    }
    
    protected void makeMeLowerFollower() {
        makeMeFollower(LOWER);
    }
    
    protected void makeMeLost() {
        // lost by default serverState.getRole().setLeader(OTHER);
        assert !serverState.getRole().isLeaderKnown();
        cluster.me(ME);
    }

    
    /* *********************************************************************
     * *
     * * Auxiliar ensure call methods
     * *
     * *********************************************************************/

        
    protected void ensureCallClockCancelSeatTo(final FlightId expectedFlight, final CustomerId expectedCustomer) {
        final TestCalls.Checker setCancelSeatTo = testCalls.exactlyOnce("it should call to Clock.cancelSeatTo");
        cluster.clock(new ClockTest() {
            @Override
            public void cancelSeatTo(FlightId flight, CustomerId customer, ServerId from) {
                assertEquals("Clock.cancelSeatTo flight", expectedFlight, flight);
                assertEquals("Clock.cancelSeatTo customer", expectedCustomer, customer);
                assertEquals("Clock.cancelSeatTo from", ME, from);
                setCancelSeatTo.called();
            }
        });
    }

    protected void ensureCallClockCancelPrepareTo(final OpId expectedOp) {
        final TestCalls.Checker setCancelSeatTo = testCalls.exactlyOnce("it should call to Clock.cancelPrepareTo");
        cluster.clock(new ClockTest() {

            @Override
            public void cancelPrepareTo(OpId op, ServerId from) {
                assertEquals("Clock.cancelPrepareTo op", expectedOp, op);
                assertEquals("Clock.cancelSeatTo from", ME, from);
                setCancelSeatTo.called();
            }
        });
    }

    protected void ensureCallClockSetElectionTo() {
        final TestCalls.Checker setElectionTo = testCalls.exactlyOnce("it should call to Clock.setElectionTo");
        cluster.clock(new ClockTest() {
            @Override
            public void setElectionTo(ServerId from) {
                assertEquals("Clock.setElectionTo from", ME, from);
                setElectionTo.called();
            }
        });
    }

                
    protected void ensureCallClockSetPrepareTo(final FlightId expectedFlight, final CustomerId expectedCustomer, final ProxyId proxyId) {
        final TestCalls.Checker setSetPrepareTo = testCalls.exactlyOnce("it should call to Clock.setPrepareTo");
        cluster.clock(new ClockTest() {
            @Override
            public void setPrepareTo(OpId opId, ServerId from) {
                assertNotNull("Clock.setPrepareTo op", opId);
                assertEquals("Clock.setPrepareTo from", ME, from);
                
                Op op = serverState.getLog().prepared(opId, Quorum.createFromTotal(2));
                assertEquals("Cluster.sendToOthers.prepare op.flight", expectedFlight, op.getFlight());
                assertEquals("Cluster.sendToOthers.prepare op.customer", expectedCustomer, op.getCustomer());
                assertEquals("Cluster.sendToOthers.prepare op.proxy", proxyId, op.getProxy());
                
                setSetPrepareTo.called();
            }
        });
    }
    
    protected void ensureCallClockSetSeatTo(final FlightId expectedFlight, final CustomerId expectedCustomer) {
        final TestCalls.Checker setSetSeatTo = testCalls.exactlyOnce("it should call to Clock.setSeatTo");
        cluster.clock(new ClockTest() {
            @Override
            public void setSeatTo(FlightId flight, CustomerId customer, ServerId from) {
                assertEquals("Clock.setSeatTo flight", expectedFlight, flight);
                assertEquals("Clock.setSeatTo customer", expectedCustomer, customer);
                assertEquals("Clock.setSeatTo from", ME, from);
                setSetSeatTo.called();
            }
        });
    }
    
    
    protected void ensureCallCustomerConfirmed(final FlightId expectedFlight, final CustomerId expectedCustomer, final boolean expected) {
        ensureCallCustomerConfirmed(expectedFlight, expectedCustomer, expectedCustomer, expected);
    }
    protected void ensureCallCustomerConfirmed(final FlightId expectedFlight, final CustomerId expectedCustomer, final ProxyId proxyId, final boolean expected) {
        final TestCalls.Checker confirmed = testCalls.exactlyOnce("it should call to Cluster.sendToCustomer.confirmed");
        cluster.customer(proxyId, new ProxyTest() {
            @Override
            public void confirmed(FlightId flight, CustomerId customer, boolean value) {
                assertEquals("Cluster.sendToCustomer.confirmed flight", expectedFlight, flight);
                assertEquals("Cluster.sendToCustomer.confirmed customer", expectedCustomer, customer);
                assertEquals("Cluster.sendToCustomer.confirmed value", expected, value);
                confirmed.called();
            }            
        });
    }

    protected void ensureCallCustomerQueried(final FlightId expectedFlight, final CustomerId expectedCustomer, final int expected) {
        final TestCalls.Checker queried = testCalls.exactlyOnce("it should call to Cluster.sendToCustomer.queried");
        cluster.customer(expectedCustomer, new ProxyTest() {
            @Override
            public void queried(FlightId flight, int freeSeats) {
                assertEquals("Cluster.sendToCustomer.queried flight", expectedFlight, flight);
                assertEquals("Cluster.sendToCustomer.queried freeSeats", expected, freeSeats);
                queried.called();
            }            
        });
    }

    protected void ensureCallLeaderPrepared(final OpId expectedOp) {
        final TestCalls.Checker prepared = testCalls.exactlyOnce("it should call to Cluster.sendToServer.prepared");
        cluster.server(OTHER, new ServerTest() {
            @Override
            public void prepared(final OpId op) {
                assertEquals("Cluster.sendToServer.prepared from", expectedOp, op);
                prepared.called();
            }
        });
    }

    protected void ensureCallLeaderSeat(final FlightId expectedFlight, final CustomerId expectedCustomer) {
        assert serverState.getRole().getLeader().equals(OTHER);
        final TestCalls.Checker confirmed = testCalls.exactlyOnce("it should call to Cluster.sendToServer.seat");
        cluster.server(OTHER, new ServerTest() {
            @Override
            public void seat(FlightId flight, CustomerId customer, ProxyId from) {
                assertEquals("Cluster.sendToServer.seat flight", expectedFlight, flight);
                assertEquals("Cluster.sendToServer.seat customer", expectedCustomer, customer);
                assertEquals("Cluster.sendToServer.seat from", ME, from);
                confirmed.called();
            }            
        });
    }

    protected void ensureCallServerElect(ServerId server, final ServerId expectedFrom) {
        final TestCalls.Checker elect = testCalls.exactlyOnce("it should call to Cluster.sendToServer."+server+".elect");
        cluster.server(server,new ServerTest() {
            @Override
            public void elect(ServerId from) {
                assertEquals("Cluster.sendToGreaters.elect from", expectedFrom, from);
                
                elect.called();
            }
        });
    }

    protected void ensureCallGreatersElect(final ServerId expectedFrom) {
        cluster.server(LOWER, new ServerTest());
        cluster.server(ME, new ServerTest());
        ensureCallServerElect(OTHER, expectedFrom);
        ensureCallServerElect(GREATER, expectedFrom);
    }

    protected void ensureCallServerApply(final ServerState serverState,final FlightId expectedFlight, final CustomerId expectedCustomer) {
        final TestCalls.Checker apply = testCalls.exactlyOnce("it should call to Cluster.sendToServer."+server+".apply");
        cluster.server(serverState.getRole().getMyId(), new ServerTest() {
            @Override
            public void apply(Nseq nseq, SubLog ops, ServerId from) {
                assertNotNull("Cluster.sendToOthers.apply nseq", nseq);
                assertNotNull("Cluster.sendToOthers.apply ops", ops);
                assertEquals("Cluster.sendToOthers.apply from", ME, from);
                
                assertTrue("Cluster.sendToOthers.apply accept nseq", serverState.getNseqTracker().isAccepted(nseq, ME));

                Nseq prevNseq = serverState.getNseqTracker().getCurrent(from);
                serverState.getNseqTracker().advance(ops, from);
                Nseq currNseq = serverState.getNseqTracker().getCurrent(from);
                assertNotSame("Cluster.sendToOthers.apply ops are not empty (nseq advances)", prevNseq, currNseq);
                
                ops.apply(serverState.getCompany());
                assertTrue("Cluster.sendToOthers.apply ops content are correct", serverState.getCompany().getFlight(expectedFlight).confirm(expectedCustomer));
                
                apply.called();
            }
        });
    }
    protected void ensureCallOthersApply(final FlightId expectedFlight, final CustomerId expectedCustomer) {
        cluster.server(ME, new ServerTest());
        ensureCallServerApply(lowerState, expectedFlight, expectedCustomer);
        ensureCallServerApply(otherState, expectedFlight, expectedCustomer);
    }

    protected void ensureCallServerPrepare(ServerId server, final FlightId expectedFlight, final CustomerId expectedCustomer, final ProxyId proxyId, final boolean checkOpId) {
        final TestCalls.Checker prepare = testCalls.exactlyOnce("it should call to Cluster.sendToServer."+server+".prepare");
        cluster.server(server,new ServerTest() {
            @Override
            public void prepare(OpId opId, ServerId from) {
                assertNotNull("Cluster.sendToOthers.prepare op", opId);
                assertEquals("Cluster.sendToOthers.prepare from", ME, from);
                
                if (checkOpId) {
                    Op op = serverState.getLog().prepared(opId, Quorum.createFromTotal(2));
                    assertEquals("Cluster.sendToOthers.prepare op.flight", expectedFlight, op.getFlight());
                    assertEquals("Cluster.sendToOthers.prepare op.customer", expectedCustomer, op.getCustomer());
                    assertEquals("Cluster.sendToOthers.prepare op.proxy", proxyId, op.getProxy());
                }

                prepare.called();
            }
        });
    }

    protected void ensureCallOthersPrepare(final FlightId expectedFlight, final CustomerId expectedCustomer, final ProxyId proxyId) {
        cluster.server(ME, new ServerTest());
        ensureCallServerPrepare(LOWER, expectedFlight, expectedCustomer, proxyId, true);
        ensureCallServerPrepare(OTHER, expectedFlight, expectedCustomer, proxyId, false);
    }

    protected void ensureCallServerAbort(final ServerId expectedServer) {
        final TestCalls.Checker abort = testCalls.exactlyOnce("it should call to Cluster.sendToServer.abort "+expectedServer);
        cluster.server(expectedServer, new ServerTest() {
            @Override
            public void abort() {
                abort.called();
            }
        });
    }

    protected void ensureCallServerApply(final Nseq expectedNseq, final SubLog expectedOps) {
        final TestCalls.Checker recover = testCalls.exactlyOnce("it should call to Cluster.sendToServer.apply");
        cluster.server(OTHER, new ServerTest() {
            @Override
            public void apply(Nseq nseq, SubLog ops, ServerId from) {
                assertEquals("Cluster.sendToServer.apply nseq", expectedNseq, nseq);
                assertEquals("Cluster.sendToServer.apply ops", expectedOps, ops);
                assertEquals("Cluster.sendToServer.apply from", ME, from);

                recover.called();
            }            
        });
    }

    protected void ensureCallServerRecover(final ServerId serverId, final Nseq expectedNseq, final boolean iAmLeader) {
        final TestCalls.Checker recover = testCalls.exactlyOnce("it should call to Cluster.sendToServer.recover");
        cluster.server(serverId, new ServerTest() {
            @Override
            public void recover(Nseq nseq, boolean leader, ServerId from) {
                assertEquals("Cluster.sendToServer.recover nseq", expectedNseq, nseq);
                assertEquals("Cluster.sendToServer.recover leader", iAmLeader, leader);
                assertEquals("Cluster.sendToServer.recover from", ME, from);

                recover.called();
            }            
        });
    }


    
    /* *********************************************************************
     * *
     * * Auxiliar ignore call methods
     * *
     * *********************************************************************/
    
    protected void ignoreCallGreatersElect() {
        cluster.server(OTHER);
        cluster.server(GREATER);
    }

    protected void ignoreCallOthersApply(ServerId server) {
        cluster.server(server,new ServerTest() {
            @Override
            public void apply(Nseq nseq, SubLog ops, ServerId from) {
            }
        });
    }
    protected void ignoreCallOthersApply() {
        cluster.server(LOWER);
        cluster.server(OTHER);
    }

    
    /* *********************************************************************
     * *
     * * Auxiliar other manipulations
     * *
     * *********************************************************************/
        
    protected Op otherPrepared(OpId opId) {
        return otherState.getLog().prepared(opId, Quorum.createFromTotal(2));
    }
    
    protected OpId otherSeat(FlightId flight, CustomerId customer) {
        return otherState.getLog().annotate(flight, customer, customer);
    }
    
    protected SubLog otherSubLog(FlightId flight, CustomerId customer) {
        OpId otherOpId = otherSeat(flight, customer);
        Op op = otherPrepared(otherOpId);
        Nseq nseq = otherState.getLog().executeOp(op, otherState.getCompany());
        return otherState.getLog().recover(nseq);
    }
    
    protected void serverCancelOps() {
        serverState.getLog().cancelOps();
    }
    
    protected Op serverPrepared(OpId opId) {
        return serverState.getLog().prepared(opId, Quorum.createFromTotal(2));
    }
    
    protected OpId serverSeat(FlightId flight, CustomerId customer) {
        return serverSeat(flight, customer, customer);
    }
    
    protected OpId serverSeat(FlightId flight, CustomerId customer, ProxyId proxyId) {
        return serverState.getLog().annotate(flight, customer, proxyId);
    }
    
    protected SubLog serverSubLog(FlightId flight, CustomerId customer) {
        OpId otherOpId = serverSeat(flight, customer);
        Op op = serverPrepared(otherOpId);
        Nseq nseq = serverState.getLog().executeOp(op, otherState.getCompany());
        return serverState.getLog().recover(nseq);
    }
    
    
}
